﻿namespace Commander.Core.Services
{
    using System;
    using System.Linq;
    using System.Reflection;
    using System.Linq.Expressions;
    using System.Reflection.Emit;
    using System.Collections.Generic;

    /// <summary>
    /// Flexible helper to create objects using compiled expr
    /// </summary>
    internal class InstanceFactory
    {
        /*
         * http://rogeralsing.com/2008/02/28/linq-expressions-creating-objects/
         * http://ayende.com/blog/3167/creating-objects-perf-implications
         * 
         */

        /// <summary>
        /// Create new instance of T with paramaters and cache compiled expr
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static T Create<T>(params object[] parameters)
        {
            return (T)Create(typeof(T), parameters);
        }

        /// <summary>
        /// Create new instance of typeOfObject with paramaters and cache compiled expr
        /// </summary>
        /// <param name="typeOfObject"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static object Create(Type typeOfObject, params object[] parameters)
        {
            return _instance.Create(typeOfObject, parameters, true);
        }

        /// <summary>
        /// Create new instance of T with paramaters
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static T CreateOnce<T>(params object[] parameters)
        {
            return (T)CreateOnce(typeof(T), parameters);
        }

        /// <summary>
        /// Create new instance of typeOfObject with paramaters
        /// </summary>
        /// <param name="typeOfObject"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static object CreateOnce(Type typeOfObject, params object[] parameters)
        {
            return _instance.Create(typeOfObject, parameters, false);
        }

        private static readonly object _lock = new object();

        private static readonly InstanceFactory _instance = new InstanceFactory();

        /// <summary>
        /// Common actovator delegate
        /// </summary>
        /// <param name="args"></param>
        /// <returns></returns>
        delegate object ObjectActivator(params object[] args);

        /// <summary>
        /// Store ObjectActivator cache
        /// </summary>
        private Dictionary<ConstructorInfo, ObjectActivator> _activatorCache = new Dictionary<ConstructorInfo, ObjectActivator>();

        /// <summary>
        /// To prevent from creation of new instance
        /// </summary>
        private InstanceFactory()
        {

        }

        private object Create(Type typeOfObject, object[] parameters, bool cacheResult)
        {
            ConstructorInfo constructor = GetSuitableConstructor(typeOfObject, parameters);
            ObjectActivator activator;

            if (!_activatorCache.TryGetValue(constructor, out activator))
            {
                //activator = CreateWithLambdaExpression(constructor);
                activator = CreateWithDynamicMethodEmit(constructor);

                if (cacheResult)
                {
                    lock (_lock)
                    {
                        if (!_activatorCache.ContainsKey(constructor)) _activatorCache.Add(constructor, activator);
                    }
                }
            }

            return activator(parameters);
        }

        /// <summary>
        /// Find suitable constructor
        /// </summary>
        /// <param name="typeOfObject"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        private ConstructorInfo GetSuitableConstructor(Type typeOfObject, object[] parameters)
        {
            Type[] typeOfparameters = parameters.Select(par => par.GetType()).ToArray();

            return typeOfObject.GetConstructor(typeOfparameters);
        }

        /// <summary>
        /// Create ObjectActivator using LambdaExpression
        /// </summary>
        /// <param name="ctor"></param>
        /// <returns></returns>
        private ObjectActivator CreateWithLambdaExpression(ConstructorInfo ctor)
        {
            ParameterInfo[] paramsInfo = ctor.GetParameters();

            ParameterExpression param = Expression.Parameter(typeof(object[]), "args");

            Expression[] argsExp = new Expression[paramsInfo.Length];

            for (int i = 0; i < paramsInfo.Length; i++)
            {
                Expression index = Expression.Constant(i);

                Expression paramAccessorExp = Expression.ArrayIndex(param, index);

                argsExp[i] = Expression.Convert(paramAccessorExp, paramsInfo[i].ParameterType);
            }

            //make a NewExpression that calls the
            //ctor with the args we just created
            NewExpression newExp = Expression.New(ctor, argsExp);

            //create a lambda with the New
            //Expression as body and our param object[] as arg
            LambdaExpression lambda = Expression.Lambda(typeof(ObjectActivator), newExp, param);

            //compile it            
            return (ObjectActivator)lambda.Compile();
        }

        /// <summary>
        /// Create ObjectActivator using DynamicMethod
        /// </summary>
        /// <param name="ctor"></param>
        /// <returns></returns>
        private ObjectActivator CreateWithDynamicMethodEmit(ConstructorInfo ctor)
        {
            ParameterInfo[] paramsInfo = ctor.GetParameters();

            DynamicMethod method = new DynamicMethod("CreateIntance", typeof(object), new[] { typeof(object[]) }, true);

            ILGenerator gen = method.GetILGenerator();

            for (int i = 0; i < paramsInfo.Length; i++)
            {
                gen.Emit(OpCodes.Ldarg_0);
                gen.Emit(OpCodes.Ldc_I4, i);
                gen.Emit(OpCodes.Ldelem_Ref);

                if (paramsInfo[i].ParameterType.IsValueType)
                    gen.Emit(OpCodes.Unbox_Any, paramsInfo[i].ParameterType);
                else
                    gen.Emit(OpCodes.Castclass, paramsInfo[i].ParameterType);
            }

            gen.Emit(OpCodes.Newobj, ctor); // new Created
            gen.Emit(OpCodes.Ret);

            return (ObjectActivator)method.CreateDelegate(typeof(ObjectActivator));
        }
    }
}
